import pandas as pd
import numpy as np
import plotly.express as px
from scipy.optimize import minimize
import pandas_datareader as web
import warnings
warnings.filterwarnings('ignore')
import plotly.graph_objects as go
import statsmodels.api as sm
from scipy import optimize
from numpy.linalg import inv
pd.options.plotting.backend = 'plotly'
from tqdm import tqdm
import math
import plotly.io as pio
pio.renderers.default = "plotly_mimetype+notebook"
prices = pd.read_excel("Data/data_AI.xlsx", sheet_name="RI", index_col=0, parse_dates=True).iloc[63:]
market_cap = pd.read_excel("Data/data_AI.xlsx", sheet_name="MV", index_col=0, parse_dates=True).iloc[63:]
sp500 = pd.read_excel("Data/data_AI.xlsx", sheet_name="Market Benchmark", index_col=0, parse_dates=True).iloc[63:]
rf = pd.read_excel("Data/data_AI.xlsx", sheet_name='Risk Free', index_col=0, parse_dates=True)["US BENCHMARK 10 YEAR DS GOVT. INDEX - TOT RETURN IND"].iloc[63:].pct_change()
col_names = prices.columns
def format(df):
"""
Format the other datasets to be used in the project.
Args: dataframe
Returns: formatted dataframe
"""
#Make sure that all the columns are named correctly
df.set_axis(col_names, axis=1, inplace=True)
#Make sure the dates are sorted
df.sort_index(inplace=True)
#Change the index name to Date
df.index.names = ['Date']
return df
#Format the other datasets
prices = format(prices)
market_cap = format(market_cap)
#Format the risk free rate
rf.sort_index(inplace=True)
rf.index.names = ['Date']
def get_var_name(variable):
"""
Returns the name of the variable.
Args: variable
Returns: variable name, string
"""
globals_dict = globals()
return [var_name for var_name in globals_dict if globals_dict[var_name] is variable][0]
def show(df):
"""
Displays the dataframe and its shape.
Args: dataframe
Returns: None
"""
#Display the variable name
display(get_var_name(df)+" :")
#Display the dataframe head
display(df.head())
#Display the dataframe shape
display(df.shape)
#Display the dataframes
show(prices)
show(market_cap)
#show(ff_df)
'prices :'
| ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND | VANGD.TTL.STK.MIF. ETF - TOT RETURN IND | VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND | VANGD.EMM.STK.IX. ETF - TOT RETURN IND | SPDR GOLD SHARES - TOT RETURN IND | VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND | ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND | ISHARES TIPS BOND - TOT RETURN IND | ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND | |
|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||
| 2005-04-01 | 104.13 | 105.73 | 96.07 | 94.20 | 96.03 | 107.61 | 123.57 | 108.44 | 121.97 |
| 2005-05-01 | 105.31 | 104.37 | 94.70 | 92.32 | 96.62 | 113.67 | 127.89 | 110.28 | 123.88 |
| 2005-06-01 | 106.96 | 108.77 | 95.28 | 95.10 | 93.58 | 119.11 | 133.54 | 112.14 | 125.46 |
| 2005-07-01 | 106.73 | 109.09 | 96.27 | 98.74 | 96.21 | 124.22 | 133.15 | 110.89 | 125.55 |
| 2005-08-01 | 105.94 | 113.29 | 100.12 | 105.88 | 97.18 | 131.88 | 129.75 | 108.71 | 124.79 |
(213, 9)
'market_cap :'
| ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND | VANGD.TTL.STK.MIF. ETF - TOT RETURN IND | VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND | VANGD.EMM.STK.IX. ETF - TOT RETURN IND | SPDR GOLD SHARES - TOT RETURN IND | VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND | ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND | ISHARES TIPS BOND - TOT RETURN IND | ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND | |
|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||
| 2005-04-01 | 1112.98 | 4278.81 | 19.61 | 34.40 | 2084.12 | 218.55 | 223.25 | 2271.67 | 2141.30 |
| 2005-05-01 | 1693.03 | 4298.21 | 92.50 | 84.84 | 2469.89 | 247.29 | 709.09 | 2301.26 | 2166.39 |
| 2005-06-01 | 1775.73 | 4553.20 | 118.00 | 126.98 | 2392.13 | 293.72 | 431.14 | 2477.56 | 2185.60 |
| 2005-07-01 | 2033.06 | 4661.19 | 119.46 | 167.36 | 2459.52 | 321.63 | 571.20 | 2540.16 | 2501.10 |
| 2005-08-01 | 2143.55 | 4854.30 | 139.58 | 217.33 | 2609.36 | 347.94 | 868.75 | 2680.08 | 2464.90 |
(213, 9)
prices.plot()
def cap_weighted(df):
"""
Calculates the cap weighted weights of a portfolio.
Args: dataframe of market caps
Returns: dataframe with cap weighted weights
"""
return df.shift(1).apply(lambda x: x/x.sum(), axis=1)
#Get the returns and log returns of all the stocks
returns = prices.pct_change()
show(returns)
logreturns = np.log(prices/prices.shift(1))
show(logreturns)
'returns :'
| ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND | VANGD.TTL.STK.MIF. ETF - TOT RETURN IND | VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND | VANGD.EMM.STK.IX. ETF - TOT RETURN IND | SPDR GOLD SHARES - TOT RETURN IND | VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND | ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND | ISHARES TIPS BOND - TOT RETURN IND | ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND | |
|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||
| 2005-04-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 2005-05-01 | 0.011332 | -0.012863 | -0.014260 | -0.019958 | 0.006144 | 0.056314 | 0.034960 | 0.016968 | 0.015660 |
| 2005-06-01 | 0.015668 | 0.042158 | 0.006125 | 0.030113 | -0.031463 | 0.047858 | 0.044179 | 0.016866 | 0.012754 |
| 2005-07-01 | -0.002150 | 0.002942 | 0.010390 | 0.038275 | 0.028104 | 0.042902 | -0.002920 | -0.011147 | 0.000717 |
| 2005-08-01 | -0.007402 | 0.038500 | 0.039992 | 0.072311 | 0.010082 | 0.061665 | -0.025535 | -0.019659 | -0.006053 |
(213, 9)
'logreturns :'
| ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND | VANGD.TTL.STK.MIF. ETF - TOT RETURN IND | VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND | VANGD.EMM.STK.IX. ETF - TOT RETURN IND | SPDR GOLD SHARES - TOT RETURN IND | VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND | ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND | ISHARES TIPS BOND - TOT RETURN IND | ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND | |
|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||
| 2005-04-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 2005-05-01 | 0.011268 | -0.012946 | -0.014363 | -0.020159 | 0.006125 | 0.054786 | 0.034363 | 0.016826 | 0.015538 |
| 2005-06-01 | 0.015547 | 0.041293 | 0.006106 | 0.029668 | -0.031969 | 0.046748 | 0.043231 | 0.016726 | 0.012674 |
| 2005-07-01 | -0.002153 | 0.002938 | 0.010337 | 0.037561 | 0.027717 | 0.042007 | -0.002925 | -0.011209 | 0.000717 |
| 2005-08-01 | -0.007429 | 0.037778 | 0.039213 | 0.069816 | 0.010032 | 0.059838 | -0.025867 | -0.019855 | -0.006072 |
(213, 9)
def get_portfolio_return(weights, ret = returns):
"""
Calculates the portfolio return.
Args: weights, dataframe of weights
ret, dataframe of returns, default is the returns of all the stocks
Returns: dataframe of portfolio returns
"""
return pd.DataFrame(weights.values*ret.values, columns=ret.columns, index=ret.index).sum(axis=1)
#Compute and show the SP500 return
sp500_return = sp500.pct_change()
sp500_return.replace(np.nan, 0, inplace=True)
show(sp500_return)
'sp500_return :'
| S&P 500 COMPOSITE - TOT RETURN IND | |
|---|---|
| nan | |
| 2005-04-01 | 0.000000 |
| 2005-05-01 | -0.008014 |
| 2005-06-01 | 0.036717 |
| 2005-07-01 | -0.005169 |
| 2005-08-01 | 0.035355 |
(213, 1)
def portfolio_cumret(returns_5):
"""
Calculates the cumulative return of a portfolio.
Args: returns, dataframe of returns (simple returns not log returns)
Returns: dataframe of cumulative returns
"""
#Calculate the cumulative return for simpe returns
price = (returns_5+1).cumprod()
#Return the cumulative return
return price
def eq_weighted(df1):
"""
Computes the equally weighted portfolio weights.
Args: df1, dataframe of the selected stocks, with values > 0 for the selected stocks and 0 for the others, can be returns, grades, index or any other dataframe
Returns: dataframe of the equally weighted portfolio weights
"""
#Create a copy of the dataframe to avoid modifying the original dataframe
df = df1.copy()
#Create a counter to count the stocks in the portfolio every month
counter = 0
#Loop over the months
for index, row in tqdm(df.iterrows()):
#Reset the counter to 0
counter = 0
#Loop over the stocks
for col in df.columns:
#If the stock has a return
if not row[col] == 0 and not math.isnan(row[col]):
#Increase the counter by 1
counter += 1
#Loop over the stocks
for col in df.columns:
#If the stock has a return, divide 1 by the counter to get the equally weighted portfolio weight
if not row[col] == 0 and not math.isnan(row[col]):
df.loc[index,col] = 1/counter
#If the stock has no return, set the portfolio weight to 0
else:
df.loc[index,col] = 0
#Return the dataframe of the equally weighted portfolio weights
return df
def minimum_variance(returns_1, windowsize = 36, lb = 0, ub = 1):
"""
Computes the minimum variance portfolio weights.
Args: returns_1, dataframe of the returns with 0 for the non-selected stocks
windowsize, integer, default = 36, number of months to compute the covariance matrix
lb, float, default = 0, lower bound for the stock weights
ub, float, default = 1, upper bound for the stock weights
Returns: dataframe of the minimum variance portfolio weights
"""
#Create a dataframe for the minimum variance portfolio weights
wMinVar = pd.DataFrame(index = returns_1.index, columns = returns_1.columns).fillna(0)
#Store the size of the covariance matrix window
covMatrixWindow = windowsize
#Store the number of periods
numberOfPeriods = wMinVar.shape[0]
#Loop over the months, starting at the end of the covariance matrix window
for row in tqdm(range(covMatrixWindow+1, numberOfPeriods)):
#Create a copy of the returns dataframe to avoid modifying the original dataframe
ret_0 =returns_1.copy()
#Replace the 0 values with NaN to avoid computing the covariance matrix with 0 values (not selected stocks)
ret_0.iloc[row] = ret_0.iloc[row].replace(0.0, np.nan)
#Select window of returns to compute covariance matrix
ret = ret_0.iloc[row-covMatrixWindow:row]
#Replace the 0 values with NaN to make sure we don't account for the non-selected stocks
ret.iloc[-1] = ret_0.iloc[row].replace(0.0, np.nan)
#Identify valid assets, i.e. those that have all returns, and also dropping the non-selected stocks for the current period
indexNan = ret.notna().all().tolist()
#Identify valid assets, i.e. those that have all returns, and also dropping the non-selected stocks for the current period
ret_noNan = ret.dropna(axis=1)
#Initialize the weights for this period
wMV = np.zeros(ret.shape[1])
#Store the number of assets
numberOfAssets = ret_noNan.shape[1]
#Compute the covariance matrix in the window
sigma = ret_noNan.cov()
#Get a vector of ones of the size of the number of assets
e = np.ones((numberOfAssets, 1))
#Get an equal weight vector
x0 = np.ones((numberOfAssets, 1)) / numberOfAssets
#Define objective function for min variance
objectiveMV = lambda x: x.T @ sigma @ x
#Define equality constraint, sum of weights = 1
Aeq = e.T
beq = 1
eq_constraint_1 = lambda x: np.dot(Aeq, x) - beq
cons_1 = ({'type': 'eq', 'fun': eq_constraint_1})
#Compute min var weights by minimizing the objective function
result = minimize(objectiveMV, x0, method='SLSQP', tol=1e-8, bounds=[(lb, ub)]*numberOfAssets, constraints=cons_1)
#Reattribute weights to the right stocks
wMV[indexNan] = result.x
#Store the weights in the dataframe in the current period
wMinVar.iloc[row] = wMV
#Return the dataframe of the minimum variance portfolio weights
return wMinVar
def maximum_diversification(returns_2, windowsize = 36, lb = 0, ub = 1):
"""
Computes the maximum diversification portfolio weights.
Args: returns_2, dataframe of the returns with 0 for the non-selected stocks
windowsize, integer, default = 36, the size of the window for the rolling covariance matrix
lb, float, default = 0, the lower bound for the stock weights
ub, float, default = 1, the upper bound for the stock weights
Returns: dataframe of the maximum diversification portfolio weights
"""
#Create a dataframe for the maximum diversification portfolio weights
wMaxDiv = pd.DataFrame(index = returns_2.index, columns = returns_2.columns).fillna(0)
#Store the size of the covariance matrix window
covMatrixWindow = windowsize
#Store the number of periods
numberOfPeriods = wMaxDiv.shape[0]
#Loop over the months, starting at the end of the covariance matrix window
for row in tqdm(range(covMatrixWindow+1, numberOfPeriods)):
#Create a copy of the returns dataframe to avoid modifying the original dataframe
ret_0 =returns_2.copy()
#Replace the 0 values with NaN to avoid computing the covariance matrix with 0 values (not selected stocks)
ret_0.iloc[row] = ret_0.iloc[row].replace(0.0, np.nan)
#Select window of returns to compute covariance matrix
ret = ret_0.iloc[row-covMatrixWindow:row]
#Replace the 0 values with NaN to make sure we don't account for the non-selected stocks
ret.iloc[-1] = ret_0.iloc[row].replace(0.0, np.nan)
#Identify valid assets, i.e. those that have all returns
indexNan = ret.notna().all().tolist()
#Remove non valid assets
ret_noNan = ret.dropna(axis=1)
#Initialize the weights for this period
wMD = np.zeros(ret.shape[1])
#Store the number of assets
numberOfAssets = ret_noNan.shape[1]
#Compute the covariance matrix in the window
sigma = ret_noNan.cov()
#Get the diagonal of the covariance matrix in a vector
sigmaVec = np.diag(sigma)
#Get a vector of ones of the size of the number of assets
e = np.ones((numberOfAssets, 1))
#Get an equal weight vector
x0 = np.ones((numberOfAssets, 1)) / numberOfAssets
#Define objective function for max diversification
funMDP = lambda x: (-1*x.T @ sigmaVec) / ((x.T @ sigma @ x) ** 0.5)
#Define equality constraint, sum of weights = 1
Aeq = e.T
beq = 1
eq_constraint_1 = lambda x: (Aeq @ x) - beq
cons_1 = ({'type': 'eq', 'fun': eq_constraint_1})
#Compute max diversification weights by minimizing the objective function
result = minimize(funMDP, x0, method='SLSQP', tol=1e-8, bounds=[(lb, ub)]*numberOfAssets, constraints=cons_1)
#Reattribute weights to the right stocks
wMD[indexNan] = result.x
#Store the weights in the dataframe in the current period
wMaxDiv.iloc[row] = wMD
#Return the dataframe of the maximum diversification portfolio weights
return wMaxDiv
def ERCfunc(w, sigma):
"""
Computes the ERC function, i.e. the objective function to be minimized in the ERC portfolio.
Args: w, array of weights
sigma, covariance matrix
Returns: float
"""
#Initialize the Value of the ERC function
x = 0
#Compute the risk contribution of each asset
R = np.dot(sigma, w)
#Loop over the assets a first time
for i in range(len(w)):
#Loop over the assets a second time
for j in range(len(w)):
#Compute the ERC function, i.e. the sum of the squared difference between the risk contribution of each asset
x = x + (w[i]*R[i] - w[j]*R[j])**2
#Return the value of the ERC function
return x
def ERC(returns_3, windowsize = 36, lb = 0, ub = 1):
"""
Computes the ERC portfolio weights.
Args: returns_3, dataframe of the returns with 0 for the non-selected stocks
windowsize, integer, default = 36, the size of the window for the rolling covariance matrix
lb, float, default = 0, lower bound for the stock weights
ub, float, default = 1, upper bound for the stock weights
Returns: dataframe of the ERC portfolio weights
"""
#Create a dataframe for the ERC portfolio weights
wERC = pd.DataFrame(index = returns_3.index, columns = returns_3.columns).fillna(0)
#Store the size of the covariance matrix window
covMatrixWindow = windowsize
#Store the number of periods
numberOfPeriods = wERC.shape[0]
#Loop over the months, starting at the end of the covariance matrix window
for row in tqdm(range(covMatrixWindow+1, numberOfPeriods)):
#Create a copy of the returns dataframe to avoid modifying the original dataframe
ret_0 =returns_3.copy()
#Replace the 0 values with NaN to avoid computing the covariance matrix with 0 values (not selected stocks)
ret_0.iloc[row] = ret_0.iloc[row].replace(0.0, np.nan)
#Select window of returns to compute covariance matrix
ret = ret_0.iloc[row-covMatrixWindow:row]
#Replace the 0 values with NaN to make sure we don't account for the non-selected stocks
ret.iloc[-1] = ret_0.iloc[row].replace(0.0, np.nan)
#Identify valid assets, i.e. those that have all returns
indexNan = ret.notna().all().tolist()
#Remove non valid assets
ret_noNan = ret.dropna(axis=1)
#Initialize the weights for this period
wERCnp = np.zeros(ret.shape[1])
#Store the number of assets
numberOfAssets = ret_noNan.shape[1]
#Compute the covariance matrix in the window and multiply by the number of assets
sigma = ret_noNan.cov() * numberOfAssets
#Get a vector of ones of the size of the number of assets
e = np.ones((numberOfAssets, 1))
#Get an equal weight vector
x0 = np.ones((numberOfAssets, 1)) / numberOfAssets
#Define objective function for ERC, calling the ERCfunc function
funERC = lambda x: ERCfunc(x, sigma)
#Define equality constraint, sum of weights = 1
Aeq = e.T
beq = 1
eq_constraint_1 = lambda x: (Aeq @ x) - beq
cons_1 = ({'type': 'eq', 'fun': eq_constraint_1})
#Compute ERC weights by minimizing the objective function
result = minimize(funERC, x0, method='SLSQP', tol=1e-8, bounds=[(lb, ub)]*numberOfAssets, constraints=cons_1)
#Reattribute weights to the right stocks
wERCnp[indexNan] = result.x
#Store the weights in the dataframe in the current period
wERC.iloc[row] = wERCnp
#Return the dataframe of the ERC portfolio weights
return wERC
def turnover(weights) :
"""
Computes the turnover of a portfolio.
Args: weights, dataframe of the portfolio weights
Returns: dataframe of the turnover
"""
#Create a dataframe for the turnover
result = pd.DataFrame(index = weights.index, columns = ["turnover"])
#Compute the turnover
result = (weights-weights.shift(1)).abs().sum(axis=1)
#Return the dataframe of the turnover
return result
def all_methods(windowsize = 36, lb = 0, ub = 1, fee = 0.001 , mc = True, ew = True, minvar = True, maxdiv = True, erc = True, feeadjusted = True):
"""
Computes the performance of all the methods. Plots the cumulative returns of the methods and display the statistics.
Args: windowsize, integer, default = 36, the size of the window for the rolling covariance matrix
lb, float, default = 0, lower bound for the stock weights
ub, float, default = 1, upper bound for the stock weights
fee, float, default = 0.001, the transaction fee
mc, boolean, default = True, if True, computes the market cap weighted portfolio
ew, boolean, default = True, if True, computes the equal weighted portfolio
minvar, boolean, default = True, if True, computes the minimum variance portfolio
maxdiv, boolean, default = True, if True, computes the maximum diversification portfolio
erc, boolean, default = True, if True, computes the ERC portfolio
feeadjusted, boolean, default = True, if True, takes into account the transaction fee
Returns: None
"""
#Create a dataframe for the cumulative returns of the portfolios
indexdf = sp500_return.copy()
#Compute the cumulative returns of the S&P 500
indexdf["SP500"] = portfolio_cumret(sp500_return)
#Drop the original column
indexdf = indexdf.drop(columns = ["S&P 500 COMPOSITE - TOT RETURN IND"])
#Create a dataframe for the mean turnover of the portfolios
meanturnover = pd.DataFrame(index = [0])
#Create a dataframe for the maximum weight of the portfolios
maxweight = pd.DataFrame(index = [0])
#Create a dataframe for the minimum weight of the portfolios
minweight = pd.DataFrame(index = [0])
#Set the mean turnover of the S&P 500 to NaN, because we don't have access to the S&P 500 composition
meanturnover["SP500"] = np.nan
#Set the maximum weight of the S&P 500 to NaN, because we don't have access to the S&P 500 composition
maxweight["SP500"] = np.nan
#Set the minimum weight of the S&P 500 to NaN, because we don't have access to the S&P 500 composition
minweight["SP500"] = np.nan
#If the cap weighted portfolio is to be computed
if mc:
#Compute the cap weighted portfolio weights
wcap = cap_weighted(market_cap)
#Compute the turnover of the cap weighted portfolio
tocap = turnover(wcap)
#Store the mean turnover of the cap weighted portfolio
meanturnover["Market cap"] = tocap.mean()
#If the transaction fees are to be taken into account
if feeadjusted:
#Store the cumulative returns of the cap weighted portfolio taking into account the transaction fees
indexdf["Market cap"] = portfolio_cumret(get_portfolio_return(wcap, returns)-tocap*fee)
#If the transaction fees are not to be taken into account
else:
#Store the cumulative returns of the cap weighted portfolio not taking into account the transaction fees
indexdf["Market cap"] = portfolio_cumret(get_portfolio_return(wcap, returns))
#Compute the maximum weight of the cap weighted portfolio
maxweight["Market cap"] = wcap.max().max()
#Compute the minimum weight of the cap weighted portfolio
minweight["Market cap"] = wcap.min().min()
#If the equal weight portfolio is to be computed
if ew:
#Compute the equal weight portfolio weights
weq = eq_weighted(returns)
#Compute the turnover of the equal weight portfolio
toeq = turnover(weq)
#Store the mean turnover of the equal weight portfolio
meanturnover["Equal weight"] = toeq.mean()
#If the transaction fees are to be taken into account
if feeadjusted:
#Store the cumulative returns of the equal weight portfolio taking into account the transaction fees
indexdf["Equal weight"] = portfolio_cumret(get_portfolio_return(weq,returns)-toeq*fee)
#If the transaction fees are not to be taken into account
else:
#Store the cumulative returns of the equal weight portfolio not taking into account the transaction fees
indexdf["Equal weight"] = portfolio_cumret(get_portfolio_return(weq,returns))
#Compute the maximum weight of the equal weight portfolio
maxweight["Equal weight"] = weq.max().max()
#Compute the minimum weight of the equal weight portfolio
minweight["Equal weight"] = weq.min().min()
#If the minimum variance portfolio is to be computed
if minvar :
#Compute the minimum variance portfolio weights
wminvar = minimum_variance(returns, windowsize, lb, ub)
#Compute the turnover of the minimum variance portfolio
tominvar = turnover(wminvar)
#Store the mean turnover of the minimum variance portfolio
meanturnover["Minimum variance"] = tominvar.mean()
#If the transaction fees are to be taken into account
if feeadjusted:
#Store the cumulative returns of the minimum variance portfolio taking into account the transaction fees
indexdf["Minimum variance"] = portfolio_cumret(get_portfolio_return(wminvar,returns)-tominvar*fee)
#If the transaction fees are not to be taken into account
else:
#Store the cumulative returns of the minimum variance portfolio not taking into account the transaction fees
indexdf["Minimum variance"] = portfolio_cumret(get_portfolio_return(wminvar,returns))
#Compute the maximum weight of the minimum variance portfolio
maxweight["Minimum variance"] = wminvar.max().max()
#Compute the minimum weight of the minimum variance portfolio
minweight["Minimum variance"] = wminvar.min().min()
#If the maximum diversification portfolio is to be computed
if maxdiv :
#Compute the maximum diversification portfolio weights
wmaxdiv = maximum_diversification(returns, windowsize, lb, ub)
#Compute the turnover of the maximum diversification portfolio
tomaxdiv = turnover(wmaxdiv)
#Store the mean turnover of the maximum diversification portfolio
meanturnover["Maximum diversification"] = tomaxdiv.mean()
#If the transaction fees are to be taken into account
if feeadjusted:
#Store the cumulative returns of the maximum diversification portfolio taking into account the transaction fees
indexdf["Maximum diversification"] = portfolio_cumret(get_portfolio_return(wmaxdiv,returns)-tomaxdiv*fee)
#If the transaction fees are not to be taken into account
else:
#Store the cumulative returns of the maximum diversification portfolio not taking into account the transaction fees
indexdf["Maximum diversification"] = portfolio_cumret(get_portfolio_return(wmaxdiv,returns))
#Compute the maximum weight of the maximum diversification portfolio
maxweight["Maximum diversification"] = wmaxdiv.max().max()
#Compute the minimum weight of the maximum diversification portfolio
minweight["Maximum diversification"] = wmaxdiv.min().min()
#If the equal risk contribution portfolio is to be computed
if erc :
#Compute the equal risk contribution portfolio weights
werc = ERC(returns, windowsize, lb, ub)
#Compute the turnover of the equal risk contribution portfolio
toerc = turnover(werc)
#Store the mean turnover of the equal risk contribution portfolio
meanturnover["ERC"] = toerc.mean()
#If the transaction fees are to be taken into account
if feeadjusted:
#Store the cumulative returns of the equal risk contribution portfolio taking into account the transaction fees
indexdf["ERC"] = portfolio_cumret(get_portfolio_return(werc,returns)-toerc*fee)
#If the transaction fees are not to be taken into account
else:
#Store the cumulative returns of the equal risk contribution portfolio not taking into account the transaction fees
indexdf["ERC"] = portfolio_cumret(get_portfolio_return(werc,returns))
#Compute the maximum weight of the equal risk contribution portfolio
maxweight["ERC"] = werc.max().max()
#Compute the minimum weight of the equal risk contribution portfolio
minweight["ERC"] = werc.min().min()
#Rebase the sample based on the windowsize
indexdf_rebased = indexdf/indexdf.iloc[windowsize+1]
indexdf_rebased = indexdf_rebased.iloc[windowsize+1:]
#Create a figure to plot the cumulative returns of the different portfolios
fig_index = go.Figure()
fig_index.add_trace(go.Scatter(x=indexdf_rebased.index, y=indexdf_rebased["SP500"], name="SP500"))
if mc:
fig_index.add_trace(go.Scatter(x=indexdf_rebased.index, y=indexdf_rebased["Market cap"], name="Market cap portfolio"))
if ew:
fig_index.add_trace(go.Scatter(x=indexdf_rebased.index, y=indexdf_rebased["Equal weight"], name="Equal weighted portfolio"))
if minvar :
fig_index.add_trace(go.Scatter(x=indexdf_rebased.index, y=indexdf_rebased["Minimum variance"], name="Minimum variance portfolio"))
if maxdiv :
fig_index.add_trace(go.Scatter(x=indexdf_rebased.index, y=indexdf_rebased["Maximum diversification"], name="Maximum diversification portfolio"))
if erc :
fig_index.add_trace(go.Scatter(x=indexdf_rebased.index, y=indexdf_rebased["ERC"], name="ERC portfolio"))
fig_index.update_yaxes(type="log")
fig_index.update_layout(title="Portfolio performance")
fig_index.show()
#Compute the returns from the rebased cumulative returns
index_ret = indexdf_rebased.pct_change()
#Rebase the risk free rate
rf_loc = rf.loc[index_ret.index]
#Compute the compound annual growth rate of the different portfolios
CAGR = (indexdf_rebased.iloc[-1]/indexdf_rebased.iloc[0])**(12/len(indexdf_rebased))-1
#Store the results in a dataframe
stats = pd.DataFrame(CAGR, columns=["CAGR"])
#Compute the volatility of the different portfolios
stats["Volatility"] = indexdf_rebased.pct_change().std()*(12**0.5)
#Compute the excess return of the different portfolios
stats["Excess return"] = index_ret.subtract(rf_loc.values, axis=0).mean()*12
#Compute the Sharpe ratio of the different portfolios
stats["Sharpe ratio"] = stats["Excess return"]/stats["Volatility"]
#Skewness
stats["Skewness"] = index_ret.skew()
#Excess Kurtosis
stats["Excess Kurtosis"] = index_ret.kurt()-3
#Semi deviation
stats["Semi deviation"] = index_ret[index_ret<0].std()*(12**0.5)
#Max drawdown
stats["Max drawdown"] = indexdf_rebased.div(indexdf_rebased.cummax()).sub(1).min()
#Value at risk 95%
stats["Value at risk 95%"] = index_ret.quantile(0.05)
#Conditional value at risk 95%
stats["Conditional value at risk 95%"] = index_ret[index_ret<index_ret.quantile(0.05)].mean()
#Compute the tracking error of the different portfolios
stats["Tracking error"] = index_ret.sub(index_ret["SP500"], axis=0).std()*(12**0.5)
#Compute the information ratio of the different portfolios
stats["Information ratio"] = (index_ret.sub(index_ret["SP500"], axis=0).mean()*12)/stats["Tracking error"]
#Store the mean turnover of the different portfolios
stats["Mean Turnover"] = meanturnover.iloc[0]
#Store the maximum weight of the different portfolios
stats["Max weight"] = maxweight.iloc[0]
#Store the minimum weight of the different portfolios
stats["Min weight"] = minweight.iloc[0]
#Format the results
stats = stats.transpose()
stats = stats.style.format({
"SP500": "{:.4f}".format,
"Market cap": "{:.4f}".format,
"Equal weight": "{:.4f}".format,
"Minimum variance": "{:.4f}".format,
"Maximum diversification": "{:.4f}".format,
"ERC": "{:.4f}".format
})
#Display the results
display("Portfolio statistics :")
display(stats)
display(returns)
| ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND | VANGD.TTL.STK.MIF. ETF - TOT RETURN IND | VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND | VANGD.EMM.STK.IX. ETF - TOT RETURN IND | SPDR GOLD SHARES - TOT RETURN IND | VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND | ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND | ISHARES TIPS BOND - TOT RETURN IND | ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND | |
|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||
| 2005-04-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 2005-05-01 | 0.011332 | -0.012863 | -0.014260 | -0.019958 | 0.006144 | 0.056314 | 0.034960 | 0.016968 | 0.015660 |
| 2005-06-01 | 0.015668 | 0.042158 | 0.006125 | 0.030113 | -0.031463 | 0.047858 | 0.044179 | 0.016866 | 0.012754 |
| 2005-07-01 | -0.002150 | 0.002942 | 0.010390 | 0.038275 | 0.028104 | 0.042902 | -0.002920 | -0.011147 | 0.000717 |
| 2005-08-01 | -0.007402 | 0.038500 | 0.039992 | 0.072311 | 0.010082 | 0.061665 | -0.025535 | -0.019659 | -0.006053 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2022-08-01 | 0.020441 | 0.079538 | 0.047447 | -0.007967 | -0.019538 | 0.058315 | 0.038299 | 0.027030 | 0.033427 |
| 2022-09-01 | -0.037935 | -0.034446 | -0.087922 | -0.003165 | -0.043403 | -0.050735 | -0.085003 | -0.034752 | -0.051870 |
| 2022-10-01 | -0.029502 | -0.069737 | -0.061334 | -0.082827 | 0.003570 | -0.114659 | -0.050439 | -0.048155 | -0.036679 |
| 2022-11-01 | -0.018882 | 0.051076 | 0.068237 | -0.027261 | -0.031373 | 0.016566 | -0.065721 | 0.001161 | -0.018501 |
| 2022-12-01 | 0.044748 | 0.055573 | 0.139236 | 0.121530 | 0.093698 | 0.059549 | 0.095421 | 0.029402 | 0.072075 |
213 rows × 9 columns
all_methods(lb = 0, ub = 1, mc = True, ew = True, minvar = True, maxdiv = True, erc = True)
213it [00:00, 1819.42it/s] 100%|██████████| 176/176 [00:10<00:00, 16.06it/s] 100%|██████████| 176/176 [00:05<00:00, 34.86it/s] 100%|██████████| 176/176 [00:03<00:00, 49.66it/s]
'Portfolio statistics :'
| SP500 | Market cap | Equal weight | Minimum variance | Maximum diversification | ERC | |
|---|---|---|---|---|---|---|
| CAGR | 0.0971 | 0.0470 | 0.0513 | 0.0294 | 0.0755 | 0.0474 |
| Volatility | 0.1848 | 0.1133 | 0.1130 | 0.0412 | 0.1416 | 0.0797 |
| Excess return | 0.0786 | 0.0204 | 0.0245 | -0.0023 | 0.0506 | 0.0175 |
| Sharpe ratio | 0.4255 | 0.1805 | 0.2168 | -0.0554 | 0.3574 | 0.2199 |
| Skewness | -0.7577 | -0.6205 | -0.2464 | -0.4882 | 2.4473 | -0.0744 |
| Excess Kurtosis | -0.9581 | 0.2305 | 1.9534 | 0.0369 | 24.8767 | 0.6598 |
| Semi deviation | 0.1495 | 0.0886 | 0.0895 | 0.0334 | 0.1019 | 0.0595 |
| Max drawdown | -0.4913 | -0.2185 | -0.2859 | -0.1398 | -0.2865 | -0.1962 |
| Value at risk 95% | -0.0857 | -0.0440 | -0.0489 | -0.0173 | -0.0368 | -0.0333 |
| Conditional value at risk 95% | -0.1267 | -0.0767 | -0.0757 | -0.0284 | -0.0824 | -0.0511 |
| Tracking error | 0.0000 | 0.1185 | 0.1110 | 0.1740 | 0.1570 | 0.1479 |
| Information ratio | nan | -0.4911 | -0.4876 | -0.4652 | -0.1783 | -0.4131 |
| Mean Turnover | nan | 0.0452 | 0.0110 | 0.1076 | 0.1545 | 0.0505 |
| Max weight | nan | 0.4659 | 0.1250 | 1.0000 | 0.6369 | 0.2455 |
| Min weight | nan | 0.0001 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |
store_returns = returns.copy()
store_market_cap = market_cap.copy()
returns = returns[["VANGD.TTL.STK.MIF. ETF - TOT RETURN IND", "VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND","VANGD.EMM.STK.IX. ETF - TOT RETURN IND"]]
market_cap = market_cap[["VANGD.TTL.STK.MIF. ETF - TOT RETURN IND", "VANGUARD EUROPEAN STOCK INDEX FUND ETF - TOT RETURN IND","VANGD.EMM.STK.IX. ETF - TOT RETURN IND"]]
all_methods(lb = 0, ub = 1, mc = True, ew = True, minvar = True, maxdiv = True, erc = True)
returns = store_returns.copy()
market_cap = store_market_cap.copy()
213it [00:00, 4955.41it/s] 100%|██████████| 176/176 [00:01<00:00, 98.10it/s] 100%|██████████| 176/176 [00:01<00:00, 100.45it/s] 100%|██████████| 176/176 [00:00<00:00, 308.34it/s]
'Portfolio statistics :'
| SP500 | Market cap | Equal weight | Minimum variance | Maximum diversification | ERC | |
|---|---|---|---|---|---|---|
| CAGR | 0.0971 | 0.0455 | 0.0448 | 0.0667 | 0.0338 | 0.0456 |
| Volatility | 0.1848 | 0.2047 | 0.2099 | 0.1837 | 0.2420 | 0.2071 |
| Excess return | 0.0786 | 0.0339 | 0.0342 | 0.0501 | 0.0309 | 0.0345 |
| Sharpe ratio | 0.4255 | 0.1655 | 0.1631 | 0.2727 | 0.1278 | 0.1664 |
| Skewness | -0.7577 | -0.5985 | -0.4941 | -0.8171 | -0.3214 | -0.5236 |
| Excess Kurtosis | -0.9581 | -1.2233 | -0.8811 | -0.4524 | -0.9515 | -0.9194 |
| Semi deviation | 0.1495 | 0.1627 | 0.1640 | 0.1530 | 0.1777 | 0.1627 |
| Max drawdown | -0.4913 | -0.5432 | -0.5569 | -0.4925 | -0.5941 | -0.5569 |
| Value at risk 95% | -0.0857 | -0.0956 | -0.1029 | -0.0832 | -0.1090 | -0.1028 |
| Conditional value at risk 95% | -0.1267 | -0.1473 | -0.1487 | -0.1304 | -0.1651 | -0.1474 |
| Tracking error | 0.0000 | 0.0634 | 0.0708 | 0.0472 | 0.1198 | 0.0667 |
| Information ratio | nan | -0.7062 | -0.6272 | -0.6040 | -0.3982 | -0.6624 |
| Mean Turnover | nan | 0.0404 | 0.0172 | 0.1019 | 0.1197 | 0.0251 |
| Max weight | nan | 0.9875 | 0.5000 | 1.0000 | 1.0000 | 0.5000 |
| Min weight | nan | 0.0045 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |
returns = returns[["ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND", "ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND", "ISHARES TIPS BOND - TOT RETURN IND", "ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND"]]
market_cap = market_cap[["ISHARES CORE US AGGREGATE BOND ETF - TOT RETURN IND", "ISHARES 20+ YR.TRSY.BOND - TOT RETURN IND", "ISHARES TIPS BOND - TOT RETURN IND", "ISHARES IBOXX $ INV GRADE CORPORATE BOND ETF - TOT RETURN IND"]]
all_methods(lb = 0, ub = 1, mc = True, ew = True, minvar = True, maxdiv = True, erc = True)
returns = store_returns.copy()
market_cap = store_market_cap.copy()
213it [00:00, 4152.62it/s] 100%|██████████| 176/176 [00:02<00:00, 67.65it/s] 100%|██████████| 176/176 [00:02<00:00, 81.16it/s] 100%|██████████| 176/176 [00:00<00:00, 391.01it/s]
'Portfolio statistics :'
| SP500 | Market cap | Equal weight | Minimum variance | Maximum diversification | ERC | |
|---|---|---|---|---|---|---|
| CAGR | 0.0971 | 0.0324 | 0.0346 | 0.0287 | 0.0368 | 0.0346 |
| Volatility | 0.1848 | 0.0599 | 0.0737 | 0.0438 | 0.1336 | 0.0737 |
| Excess return | 0.0786 | 0.0016 | 0.0047 | -0.0029 | 0.0129 | 0.0047 |
| Sharpe ratio | 0.4255 | 0.0266 | 0.0636 | -0.0657 | 0.0965 | 0.0636 |
| Skewness | -0.7577 | -0.1365 | 0.1641 | -0.2513 | 0.4173 | 0.1641 |
| Excess Kurtosis | -0.9581 | -0.9164 | -1.3742 | -0.6457 | -1.9211 | -1.3742 |
| Semi deviation | 0.1495 | 0.0433 | 0.0472 | 0.0330 | 0.0752 | 0.0472 |
| Max drawdown | -0.4913 | -0.1964 | -0.2228 | -0.1395 | -0.3413 | -0.2228 |
| Value at risk 95% | -0.0857 | -0.0261 | -0.0326 | -0.0198 | -0.0499 | -0.0326 |
| Conditional value at risk 95% | -0.1267 | -0.0396 | -0.0444 | -0.0295 | -0.0744 | -0.0444 |
| Tracking error | 0.0000 | 0.1829 | 0.1965 | 0.1826 | 0.2441 | 0.1965 |
| Information ratio | nan | -0.4211 | -0.3763 | -0.4464 | -0.2693 | -0.3763 |
| Mean Turnover | nan | 0.0385 | 0.0047 | 0.0912 | 0.0618 | 0.0047 |
| Max weight | nan | 0.5422 | 0.2500 | 1.0000 | 1.0000 | 0.2500 |
| Min weight | nan | 0.0007 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |
returns = returns[["SPDR GOLD SHARES - TOT RETURN IND","VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND"]]
market_cap = market_cap[["SPDR GOLD SHARES - TOT RETURN IND","VANGUARD REAL ESTATE INDEX FUND ETF - TOT RETURN IND"]]
all_methods(lb = 0, ub = 1, mc = True, ew = True, minvar = True, maxdiv = True, erc = True)
returns = store_returns.copy()
market_cap = store_market_cap.copy()
213it [00:00, 7001.30it/s] 100%|██████████| 176/176 [00:00<00:00, 177.04it/s] 100%|██████████| 176/176 [00:00<00:00, 205.77it/s] 100%|██████████| 176/176 [00:00<00:00, 367.51it/s]
'Portfolio statistics :'
| SP500 | Market cap | Equal weight | Minimum variance | Maximum diversification | ERC | |
|---|---|---|---|---|---|---|
| CAGR | 0.0971 | 0.0567 | 0.0690 | 0.0659 | 0.0719 | 0.0644 |
| Volatility | 0.1848 | 0.1543 | 0.1785 | 0.1530 | 0.1945 | 0.1694 |
| Excess return | 0.0786 | 0.0352 | 0.0509 | 0.0438 | 0.0563 | 0.0451 |
| Sharpe ratio | 0.4255 | 0.2283 | 0.2853 | 0.2862 | 0.2897 | 0.2662 |
| Skewness | -0.7577 | -0.2082 | 0.0894 | -0.2437 | 0.6685 | -0.2402 |
| Excess Kurtosis | -0.9581 | -0.5495 | 5.6273 | 1.9550 | 8.7418 | 4.7048 |
| Semi deviation | 0.1495 | 0.1057 | 0.1351 | 0.1078 | 0.1449 | 0.1302 |
| Max drawdown | -0.4913 | -0.2614 | -0.3829 | -0.3062 | -0.3793 | -0.3829 |
| Value at risk 95% | -0.0857 | -0.0561 | -0.0576 | -0.0525 | -0.0610 | -0.0561 |
| Conditional value at risk 95% | -0.1267 | -0.0957 | -0.1150 | -0.0888 | -0.1223 | -0.1075 |
| Tracking error | 0.0000 | 0.1887 | 0.1484 | 0.1792 | 0.1518 | 0.1546 |
| Information ratio | nan | -0.2300 | -0.1868 | -0.1945 | -0.1468 | -0.2170 |
| Mean Turnover | nan | 0.0279 | 0.0141 | 0.0514 | 0.0434 | 0.0153 |
| Max weight | nan | 0.9708 | 1.0000 | 1.0000 | 1.0000 | 1.0000 |
| Min weight | nan | 0.0292 | 0.0000 | 0.0000 | 0.0000 | 0.0000 |